Although
the options that WCF offers for authentication are helpful, there are
always gaps through which specific requirements will fall. It is not
possible to guarantee that all the available choices will cover every
possible scenario, so in the typical WCF manner, you can extend the
authentication process with your own custom mechanism. This section
describes the process for doing this, along with some of the
ramifications.
First, to use custom authentication, the client credential type must be set to UserName.
This enables the username and password to be submitted to the service
to perform the authentication. A side effect is that with a UserName
client credential type, WCF requires the service to reference a
certificate that contains a public/private key pair. The public key
portion of the certificate then encrypts the credentials before they
are transmitted to the service.
The default, when the client credential type is UserName,
is for the service to use Windows to perform the authentication. To
intercept this process, the starting point is to create a class that
derives from the UserNamePasswordValidator class, which is in the System.IdentityModel.Selectors namespace. Within this class, the authentication mechanism is introduced into the process by overriding the Validate method, which is involved when WCF is in the middle of authentication.
Probably the most interesting aspect of the Validate
method is that it doesn’t return a Boolean value. In fact, this method
call doesn’t return any value at all. If the method completes, WCF
assumes that the credentials were valid. To invalidate the credentials,
a SecurityTokenValidationException exception must be raised. An example of such a class can be seen in the following code:
' VB
Public Class CustomAuthenticator
Inherits UserNamePasswordValidator
Public Overrides Sub Validate(userName As String, password As String)
If (userName <> "anyuser" OR password <> "good") Then
Throw New SecurityTokenValidationException("Invalid credentials")
End If
End Sub
End Class
// C#
public class CustomAuthenticator : UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
if (userName != "anyuser" || password != "good")
throw new SecurityTokenValidationException("Invalid credentials");
}
}
After the Validator
class has been created, the next step is to configure the service to
use its functionality. You do this by specifying the validator type as
part of the service’s behavior configuration. The following segment
from a configuration file defines a service behavior that does just
this:
<serviceBehaviors>
<behavior name="CustomValidator">
<serviceCredentials>
<userNameAuthentication
userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType=
"ThisAssembly.CustomAuthenticator, ThisAssembly"/>
<serviceCertificate
findValue="localhost" x509FindType="FindBySubjectName"
storeLocation="CurrentUser" storeName="My" />
</serviceCredentials>
</behavior>
</serviceBehaviors>
The userNameAuthentication element contains the details that specify the custom authentication module. Although the serviceCertificate
element doesn’t have anything to do directly with the custom
authentication, it is one of the techniques you can use to provide the
certificate information to encode the credentials for transmission.
Note also that the
behavior just defined is not a part of any service by default. The
service must specify this behavior by setting the behaviorConfiguration attribute to the name of the behavior (CustomValidator in this example). Also, the client must ensure that the client credential type is set to UserName.
To actually provide the credentials requires just a small piece of coding on the client side. If you are using the ChannelFactory
class to create a proxy, the following code will submit a set of
credentials with the request, and it will use the configuration
information associated with the endpoint that has a name attribute of DemoEndpoint:
' VB
Dim factory As New _
ChannelFactory(Of IUpdateService)("DemoEndpoint")
factory.Credentials.UserName.UserName = "anyuser"
factory.Credentials.UserName.Password = "good"
// C#
ChannelFactory<IUpdateService> factory =
new ChannelFactory<IUpdateService>("DemoEndpoint");
factory.Credentials.UserName.UserName = "anyuser";
factory.Credentials.UserName.Password = "good";
Note:
When using custom authentication, you cannot specify the username and
password automatically through configuration. The credentials must be
assigned explicitly.
It is possible that you
might have to specify a Domain Name System (DNS) identity to use the
certificate. This might be necessary if the client authenticates the
service’s certificate prior to sending a request. This type of problem
is indicated through a message similar to the following:
Identity check failed for outgoing message. The expected DNS identity of the remote endpoint
was 'X' but the remote endpoint provided DNS claim 'Y'. If this is a legitimate remote
endpoint, you can fix the problem by explicitly specifying DNS identity 'Y' as the Identity
property of EndpointAddress when creating channel proxy.
The solution, as the message quite nicely suggests, is to set the identity explicitly for the DNS. You can do this by adding an identity element to the endpoint definition within the client’s configuration file, as shown here:
<identity>
<dns value="Y"/>
</identity>